Repository navigation
fix(wr-agent): a resolve answers at a deadline, so a hung mount can't hold a read slot (#334) - #346
Conversation
… hold a read slot (#334) A `resolve` walks through whatever a committed link points at, and on a hung network mount that walk can block in the kernel for as long as the mount does. Its request thread held a read slot and a permit the whole time, so four clicks left every read Busy until the agent restarted. The walk now runs on its own thread, and `resolve` answers after 10 seconds whether or not it has returned. The request thread gives back its read slot and permit when it answers; a walk left behind holds neither, so reads and idle exit carry on. Once four walks are counted as left behind, `resolve` answers Busy without starting another; up to seven can be parked, since walks already waiting can still be left behind. A hand-off is refused while any walk is left behind. A kill doesn't always end one: a FUSE request the daemon has already read waits uninterruptibly (`request_wait_answer` in Linux's fs/fuse/dev.c), and a hand-off's execve waits for every other thread to die, so it would freeze every session on the host. That keeps the hand-off behaviour the held permit used to give. The walk thread is started with `thread::Builder`, so a thread the OS refuses answers Busy instead of leaving the app to wait out its own timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A click stops at the first resolve that times out: every candidate of the same word resolves the same prefix through its last `..`, so each would hang another 10 s and leave another walk behind. The agent's timeout text is a constant, pinned by a test, that the app matches. - Tests for a walk that panics, before and after its deadline, and for `walks_left_behind()`. - Comments say a resolve holds its slot and permit until its reply is written, as a read does; the deadline only stops a hung walk stretching it. - The design doc lists a stuck file lookup among the hand-off refusals and says what a remote host shows while one is stuck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The with_deadline doc and the design doc now say a resolve timeout ends the click, that a mount slow to wake can take a second click, and that only a remote host meets a stuck file lookup at hand-off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fix (#334) From the adversarial review: a click stopped at the first resolve that timed out, assuming every candidate of a word resolves the same prefix. A `:12` before the `..` breaks that (`good.rb:12/link/../t.rb` also gives plain `good.rb`), so a hung link could suppress a healthy file. The click now skips only candidates through the same prefix as one that timed out. The agent logs each walk it leaves behind and each one that returns, and the design doc says a restart can't clear a walk parked in a FUSE wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The re-review of the previous commit found that skipping only candidates through the same prefix lets a later candidate with no `..` read through the same hung link (`sub/link` from `sub/link/..`), and `read` has no deadline (#343), so one click could hold a read slot forever. Back to ending the click at the first resolve that times out: nothing more in that click reaches the host. The contrived word whose `:line` sits before its `..` loses its healthy variant again; tracked as #345, to revisit once reads have a deadline. The log lines and the note that a restart can't clear a FUSE-parked walk stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Hand-off safety needs human validation against hung mounts, and the resolve-cap fallback still permits an unbounded read.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Addresses #334 by bounding remote path resolution so hung mounts cannot indefinitely retain resolve read slots.
Changes:
- Adds a 10-second resolve deadline, abandoned-walk accounting, and hand-off protection.
- Stops remote cmd-click probing after a resolve timeout.
- Adds regression coverage and documents operational limits.
| File | Description |
|---|---|
vcs/crates/wr-agent/src/handoff.rs |
Refuses hand-off while abandoned walks remain. |
vcs/crates/wr-agent/src/file.rs |
Adds deadlines, walk limits, logging, and tests. |
TODOS.md |
Records an unrelated relay-test flake. |
macapp/WorkroomAppTests/TerminalLinkOpenerTests.swift |
Tests timeout-driven click termination. |
macapp/WorkroomApp/Core/TerminalSessions.swift |
Stops candidate probing after resolve timeouts. |
macapp/WorkroomApp/Core/AgentFileProvider.swift |
Defines the timeout-message prefix. |
docs/designs/remote-workrooms.md |
Documents deadlines, hand-off refusal, and limitations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } catch FileServiceError.failed(let message) | ||
| where message.hasPrefix(AgentFileProvider.resolveTimedOut) | ||
| { | ||
| // The walk hung on a mount (#334). The other candidates differ only after the last | ||
| // `..`, so they would each hang another 10 s and leave another walk behind. | ||
| NSLog("Workroom: resolving %@ on the host timed out", onHost) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
🤖 Left by Claude Code.
Fixed in bed7713. The cap refusal (too many earlier resolves are still walking) now ends the click as a timeout does: TerminalSessions.remoteFile matches AgentFileProvider.resolveWalksBusy alongside resolveTimedOut and returns before any later candidate is probed.
- The agent's cap test now asserts that exact text, so the two sides can't drift apart.
- A new case in
testARemoteParentTraversalIsResolvedOnTheHostcoverslink/file.rb:12/../t.rb. The literal candidate gets the cap refusal, andlink/file.rbexists, so a fallback read would return it. The test asserts nil, and it went red with the new match removed.
From Copilot's review of #346: once four walks are stuck, `resolve` answers Busy("too many earlier resolves are still walking"), which missed the timeout-only catch. The click then went on to the next candidate, and for a word like `link/file.rb:12/../t.rb` that is plain `link/file.rb`, a read through the same hung link with no deadline (#343). The app now ends the click on that refusal as on a timeout. Its text is pinned by the cap test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Summary
Closes #334.
A remote cmd-click on
link/../file.rbmakes the agent's File serviceresolvethe path on the host (#327). If the link leads onto a hung NFS or FUSE mount,canonicalizeblocks for as long as the mount does. The request thread held one of the four read slots and a permit the whole time, so four clicks left every file read on the hostBusyuntil the agent restarted.wr-agent
resolveruns its walk on its own thread (with_deadline) and answers after 10 seconds whether or not the walk has returned. The answer is anIotimeout with a fixed prefix,RESOLVE_TIMED_OUT. Its request thread holds its read slot and permit until the reply is written, and a hung walk holds neither, so reads and idle exit carry on.ABANDONED_WALKS, using a per-walk state word, and a panic in the walk still updates the count. Once four are counted,resolveanswersBusywithout starting another. Up to seven can be parked, since walks already waiting can still be left behind.hand_offis refused while any walk is left behind. A FUSE request the daemon has already read waits uninterruptibly (request_wait_answerin Linux'sfs/fuse/dev.c), andexecvewaits for every other thread to die, so the hand-off would freeze every session on the host. That keeps the behaviour the held permit used to give. The check is race-free: withQuietheld, no request starts.thread::Builder, so if the OS refuses a thread the answer isBusyinstead of a panic.macapp
AgentFileProvider.resolveTimedOut). Its other candidates resolve the same prefix, or would read through the same link with no deadline, so nothing more in that click reaches the host. One click on a dead mount therefore costs 10 seconds and one walk, instead of up to 40 seconds and four walks.Docs:
docs/designs/remote-workrooms.md"Path handling" and the hand-off section cover:readthrough a link onto a hung mount holds a read slot and permit forever #343 and A remote cmd-click whose:linecomes before its..loses its healthy variant when the resolve times out #345.Follow-ups filed (all part of #154):
readthrough a link onto a hung mount holds a read slot and permit forever #343:readhas no deadline. A plainlink/file.rbclick onto a hung mount still holds a read slot forever. This is the most exploitable remaining finding.:linecomes before its..loses its healthy variant when the resolve times out #345: a word whose:linecomes before its..(good.rb:12/link/../t.rb) loses its healthygood.rbvariant when the resolve times out.Accepted trade-offs from the adversarial review:
Test Coverage
with_deadlinewaiting for the walk (fails in 7 s, it doesn't hang);catch_unwindremoved;walks_left_behind()returning 0;"resolving timed out"that the app matches.Pre-Landing Review
Before /ship: the independent adversarial pass that macapp/AGENTS.md rule 3 requires for any
wr-agentchange found five issues, all fixed. The serious one: a hand-off couldexecvebehind an uninterruptible FUSE walk and freeze every session. The other four:/ship review: this ran past the skill's 3-round fix limit, with the owner approving each extra round. The review log records
cycles: 5, converged: false...read through the hung link with no deadline, so it was reverted to "a timeout ends the click" at the owner's call. The lost-click case is filed as A remote cmd-click whose:linecomes before its..loses its healthy variant when the resolve times out #345.bdaf2a3c) only by the log lines, which the lean re-review covered, and by comment, doc and TODO wording.Step 11 adversarial:
readthrough a link onto a hung mount holds a read slot and permit forever #343 and A remote cmd-click that times out on a hung mount shows the user nothing #344; the restart-docs and logging items were fixed.:linecomes before its..loses its healthy variant when the resolve times out #345.Exploratory QA
Functional surface: the wr-agent File service
resolve.resolvetests pass 6/6.with_deadline, and the hand-off refusal was checked by reading the code.Design Review
No frontend files changed — design review skipped.
Eval Results
No prompt-related files changed — evals skipped.
Scope Drift
Scope Check: CLEAN. Beyond #334:
Plan Completion
No plan file detected. The plan was #334's "How to fix", the request, and the independent review's findings: 12 items, 9 DONE, 1 CHANGED. The CHANGED item: it detaches the walk (#334's first option) rather than walking components one at a time with openat. 2 items the diff can't show were confirmed by evidence:
fs/fuse/dev.cfetched from torvalds/linux;Verification Results
No plan-specific verification items. Automatic diff-scoped QA is above.
TODOS
No TODO items completed in this PR. Added one P0:
a_relay_with_no_agent_fails_fast_and_says_soflaked once while the full Swift suite ran alongside it. It passed 5/5 alone, and this branch doesn't touch the relay.Documentation
Status: current. No edits needed.
Scope: committed diff 4862781..b44aa65, with nothing staged, unstaged or untracked. Docs audited: README.md, CONTRIBUTING.md, AGENTS.md, macapp/AGENTS.md, macapp/README.md, docs/repository-notes.md, docs/designs/remote-workrooms.md and TODOS.md (read only). docs/agents, DESIGN.md, PRODUCT.md, the other docs/designs files and .agents/skills were searched for File service, hand-off refusal and remote cmd-click content, and none was affected.
Documentation health:
readthrough a link onto a hung mount holds a read slot and permit forever #343 and A remote cmd-click whose:linecomes before its..loses its healthy variant when the resolve times out #345 caveats), the hand-off prerequisite bullet and the "Known limits" retry note all matchfile.rs,handoff.rs,TerminalSessions.swiftandAgentFileProvider.swift.resolvethrough a link onto a dead mount holds a File read slot until the agent restarts #334 follow-up entry is parent-owned.Coverage debt: #334 has reference and explanation coverage in remote-workrooms.md. There is no how-to or tutorial page, which is acceptable for an internal agent behaviour. No gaps were filled. No architecture diagrams are affected.
Test plan
cargo test --manifest-path vcs/Cargo.toml --workspace: all suites pass onb44aa653(run alone; see TODOS for the relay flake under parallel load)make app-test: full Swift unit suite passes onb44aa653cargo fmt --check,cargo clippy -D warnings,make app-lint: clean🤖 Generated with Claude Code